/*
fxp_expat.cpp: Expat implementation file for the Fudepan XML Parser project.
    Copyright (C) 2009, 2010  Daniel Gutson, FuDePAN

    This file is part of the FXP project.

    FXP is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    FXP is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with FXP.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <fstream>
#include <string>
#include <stack>
#include <iostream>
#include <sstream>
#include <expat.h>

#include "fx-parser/fxp.h"

using namespace std;

namespace fxp
{

struct FXPExpatData
{
    struct LevelInfo
    {
        FXPListener* listener;
        string accumulatedText;
        size_t textStartedAt;
        string current_element;  /* mostly for error messages */
    };
    stack<LevelInfo> levels;
    TextAction textAction;
    ErrorAction unknownStuffAction;
    ErrorAction malformedStuffAction;
    FXPListener* const dumbListener;
    stringstream error;
    bool ok;
    size_t lineNumber;

    FXPExpatData(TextAction textAction, ErrorAction unknownStuffAction, ErrorAction malformedStuffAction)
        : textAction(textAction),
          unknownStuffAction(unknownStuffAction),
          malformedStuffAction(malformedStuffAction),
          dumbListener(new FXPDumbListener),
          ok(true),
          lineNumber(0)
    {}

    ~FXPExpatData()
    {
        delete dumbListener;
    }

    void accumulateText(const string& text)
    {
        if (levels.top().accumulatedText.empty())
            levels.top().textStartedAt = lineNumber;

        levels.top().accumulatedText += text;
    }

    FXPListener* current_listener() const
    {
        return levels.top().listener;
    }

    bool has_levels() const
    {
        return !levels.empty();
    }

    void push_level(FXPListener* new_listener, const string& current_element)
    {
        levels.push(LevelInfo());
        levels.top().current_element = current_element;
        levels.top().listener = new_listener;
    }

    void pop_level()
    {
        levels.pop();
    }

    string& accumulatedText()
    {
        return levels.top().accumulatedText;
    }
};

static void XMLCALL
startElement(void* userData, const char* name, const char** atts)
{
    FXPExpatData* data = static_cast<FXPExpatData*>(userData);

    if (data->has_levels())
    {
        FXPListener* new_listener = data->current_listener()->new_child(name);

        if (new_listener != NULL)
        {
            size_t attr(0);

            data->push_level(new_listener, name);

            while (atts[attr] != NULL && data->ok)
            {
                switch (new_listener->new_attribute(atts[attr], atts[attr + 1]))
                {
                    case FXPListener::AttributeAccepted:
                        break;

                    case FXPListener::MalformedAttributeValue:
                        switch (data->malformedStuffAction)
                        {
                            case Stop:
                                data->ok = false;
                                data->error << "Malformed attribute '" << atts[attr] << "' with \"" << atts[attr + 1] << "\"";
                                break;

                            case Warn:
                                cerr << "Malformed attribute '" << atts[attr] << "' with \"" << atts[attr + 1] << "\"" << endl;
                            case Ignore:
                                break;
                        }
                        break;

                    case FXPListener::UnknownAttribute:
                        switch (data->unknownStuffAction)
                        {
                            case Stop:
                                data->error << "Unknown attribute '" << atts[attr] << "' in element '" << name << "'";
                                data->ok = false;
                                break;

                            case Warn:
                                cerr << "Unknown attribute '" << atts[attr] << "' in element '" << name << "'" << endl;
                            case Ignore:
                                break;
                        }
                        break;
                }

                attr += 2;
            }
        }
        else
        {
            switch (data->unknownStuffAction)
            {
                case Stop:
                    data->error << "Unknown XML element '" << name << "' child of '" << data->levels.top().current_element << "'";
                    data->ok = false;
                    break;

                case Warn:
                    cerr << "Warning: Unknown XML element '" << name << "' child of '" << data->levels.top().current_element << "'" << endl;
                case Ignore:
                    data->push_level(data->dumbListener, "");
            }
        }
    }
    else
    {
        data->error << "Unexpected Error: Malformed XML file, closing '" << name << "' without opening tag.";
        data->ok = false;
    }
}

static void XMLCALL
endElement(void* userData, const char* name)
{
    FXPExpatData* data = static_cast<FXPExpatData*>(userData);

    if (data->has_levels())
    {
        // Check pending text
        if (data->textAction != DisableTextProcessing && !data->accumulatedText().empty())
        {
            if (!data->current_listener()->receive_text(data->accumulatedText()))
            {
                switch (data->textAction)
                {
                    case StopOnUnacceptedText:
                        data->error << "Unexpected text";
                        data->ok = false;
                        break;

                    case WarnOnUnacceptedText:
                        cerr << "Warning: Unexpected text at line " << data->levels.top().textStartedAt << endl;
                    case IgnoreUnacceptedText:
                    default:
                        break;
                }
            }
            data->accumulatedText().clear();
        }

        data->current_listener()->close();
        data->pop_level();
    }
    else
    {
        data->error << "Error: Malformed XML file, closing '" << name << "' without opening tag.";
        data->ok = false;
    }
}

static void XMLCALL
charHandler(void* userData, const XML_Char* s, int len)
{
    FXPExpatData* data = static_cast<FXPExpatData*>(userData);

    if (data->has_levels())
    {
        data->accumulateText(string(s, len));
    }
    else
    {
        data->error << "Error: Malformed XML file, receiving text after last element.";
        data->ok = false;
    }
}

bool parse_xml(
    const char* filename,
    FXPListener* listener,
    ParseErrorData& ped,
    ErrorAction unknownStuff,
    ErrorAction malformedStuff,
    TextAction textAction)
{
    ifstream file(filename);

    ped.error.clear();

    if (file.is_open())
    {
        string line;
        FXPExpatData data(textAction, unknownStuff, malformedStuff);
        XML_Parser parser = XML_ParserCreate(NULL);
        data.push_level(listener, "root");

        XML_SetUserData(parser, &data);
        XML_SetElementHandler(parser, startElement, endElement);

        if (textAction != DisableTextProcessing)
            XML_SetCharacterDataHandler(parser, charHandler);

        while (getline(file, line) && data.ok)
        {
            data.lineNumber++;
            if (XML_Parse(parser, line.c_str(), line.size(), file.eof()) == XML_STATUS_ERROR)
            {
                ped.error = (const char*)XML_ErrorString(XML_GetErrorCode(parser));
                data.ok = false;
            }
        }

        if (!data.ok)
        {
            ped.line = data.lineNumber;
            if (ped.error.empty())
                ped.error = data.error.str();
        }

        XML_ParserFree(parser);

        return data.ok;
    }
    else
    {
        ped.error = "File not found.";
        return false;
    }
}

}
